package org.carabiner.util;

import java.awt.AWTEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.InputMethodEvent;
import java.awt.event.InputMethodListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.awt.event.WindowListener;
import java.awt.event.WindowStateListener;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;

import junit.framework.AssertionFailedError;

/**
 * Allows JUnit tests to safely wait for changes to swing components that are
 * asynchronously queued up on the Java event queue. This is necessary because
 * (normally) you're not supposed to modify swing components on any thread other
 * than the event thread, but JUnit doesn't run on the event thread.<p/>
 * 
 * Here's an example: <code><pre>
 * JFrame frame = new JFrame();
 * JPanel panel = new JPanel();
 * SwingSync swingSync = new SwingSync(ComponentEvent.COMPONENT_RESIZED);
 * panel.addComponentListener(swingSync);
 * Dimension size = new Dimension(200, 200);
 * panel.setPreferredSize(size);
 * frame.getContentPane().add(panel);
 * frame.pack();
 * frame.setVisible(true);
 * swingSync.waitForEvent();
 * assertEquals(&quot;Swing failed to change the panel size&quot;, size, panel.getSize());
 * </pre></code>
 * 
 * @author <a href="mailto:ben@int.com>Ben Rady</a>
 * @version 0.3
 */
public class SwingSync implements ActionListener, AdjustmentListener,
    AncestorListener, ComponentListener, HierarchyListener,
    InputMethodListener, ItemListener, WindowListener, WindowFocusListener,
    WindowStateListener
{
  private int id;

  private Class clazz;

  private long timeout = 2000;

  private Set events;

  private SwingSync()
  {
    events = new HashSet();
    id = Integer.MIN_VALUE;
  }

  /**
   * Constructs a SwinSync object that will wait for an AWTEvent. The event type
   * is specified by an ID like <code>ComponentEvent.COMPONENT_RESIZED</code>.
   * 
   * @param eventId
   *          int
   */
  public SwingSync(int eventId)
  {
    this();
    id = eventId;
  }

  /**
   * Constructs a SwinSync object that will wait for an AWTEvent. The event type
   * is specified by a class like <code>ActionEvent.class</code>.
   * 
   * @param classIdent
   *          int
   */
  public SwingSync(Class classIdent)
  {
    this();
    clazz = classIdent;
  }

  /**
   * This call will block until the event is received. Will throw
   * SyncFailedException
   * 
   * @throws InterruptedException
   * @throws AssertionFailedError
   *           if the wait times out.
   */
  public synchronized void waitForEvent() throws InterruptedException,
      AssertionFailedError
  {
    long timestamp = System.currentTimeMillis();
    while (!receivedEvent())
    {
      wait(timeout);
      if (System.currentTimeMillis() - timestamp >= timeout)
      {
        throw createWaitFailedException();
      }
    }
    events.clear();
  }

  /**
   * Whether or not the expected event has been returned
   * 
   * @return boolean
   */
  private boolean receivedEvent()
  {
    for (Iterator i = events.iterator(); i.hasNext();)
    {
      AWTEvent event = (AWTEvent) i.next();
      if (clazz != null)
      {
        if (clazz.equals(event.getClass()))
        {
          return true;
        }
      } else if (event.getID() == id)
      {
        return true;
      }
    }
    return false;
  }

  private AssertionFailedError createWaitFailedException()
  {
    if (clazz == null)
    {
      return new AssertionFailedError("Event with id " + id
          + " was never received");
    }
    return new AssertionFailedError("Event with class type " + clazz
        + " was never received");
  }

  /**
   * Sets the timeout property. See {@link #getTimeout()}
   * 
   * @param newTimeout
   *          long
   */
  public void setTimeout(long newTimeout)
  {
    timeout = newTimeout;
  }

  /**
   * Returns the amount of time (in milliseconds) that waitForEvent() will wait
   * before firing an AssertionFailedError. The default value is 2000ms
   * 
   * @return long
   */
  public long getTimeout()
  {
    return timeout;
  }

  private synchronized void processEvent(AWTEvent event)
  {
    events.add(event);
    if (clazz != null)
    {
      if (clazz.equals(event.getClass()))
      {
        notifyAll();
      }
    } else if (event.getID() == id)
    {
      notifyAll();
    }
  }

  // ///////////////////////////////////////////////////////////////////////////
  // Event Handlers
  // ///////////////////////////////////////////////////////////////////////////

  public void componentHidden(ComponentEvent e)
  {
    processEvent(e);
  }

  public void componentMoved(ComponentEvent e)
  {
    processEvent(e);
  }

  public void componentResized(ComponentEvent e)
  {
    processEvent(e);
  }

  public void componentShown(ComponentEvent e)
  {
    processEvent(e);
  }

  public void windowOpened(WindowEvent e)
  {
    processEvent(e);
  }

  public void windowClosing(WindowEvent e)
  {
    processEvent(e);
  }

  public void windowClosed(WindowEvent e)
  {
    processEvent(e);
  }

  public void windowIconified(WindowEvent e)
  {
    processEvent(e);
  }

  public void windowDeiconified(WindowEvent e)
  {
    processEvent(e);
  }

  public void windowActivated(WindowEvent e)
  {
    processEvent(e);
  }

  public void windowDeactivated(WindowEvent e)
  {
    processEvent(e);
  }

  public void windowGainedFocus(WindowEvent e)
  {
    processEvent(e);
  }

  public void windowLostFocus(WindowEvent e)
  {
    processEvent(e);
  }

  public void windowStateChanged(WindowEvent e)
  {
    processEvent(e);
  }

  public void actionPerformed(ActionEvent e)
  {
    processEvent(e);
  }

  public void adjustmentValueChanged(AdjustmentEvent e)
  {
    processEvent(e);
  }

  public void ancestorAdded(AncestorEvent event)
  {
    processEvent(event);
  }

  public void ancestorMoved(AncestorEvent event)
  {
    processEvent(event);
  }

  public void ancestorRemoved(AncestorEvent event)
  {
    processEvent(event);
  }

  public void hierarchyChanged(HierarchyEvent e)
  {
    processEvent(e);
  }

  public void caretPositionChanged(InputMethodEvent event)
  {
    processEvent(event);
  }

  public void inputMethodTextChanged(InputMethodEvent event)
  {
    processEvent(event);
  }

  public void itemStateChanged(ItemEvent e)
  {
    processEvent(e);
  }
}
